//===============================================================================
// Microsoft Aspiring Software Architects Program
// E-Appointments - Case Study Implementation
//===============================================================================
// Copyright  Microsoft Corporation.  All rights reserved.
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY
// OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT
// LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS FOR A PARTICULAR PURPOSE.
//===============================================================================
// The example companies, organizations, products, domain names,
// e-mail addresses, logos, people, places, and events depicted
// herein are fictitious.  No association with any real company,
// organization, product, domain name, email address, logo, person,
// places, or events is intended or should be inferred.
//===============================================================================

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using System.Diagnostics;
using System.Windows.Media.Animation;
using System.IO;
using System.Windows.Threading;
using System.Threading;
using System.Collections.Specialized;
using System.Collections;

namespace EAppointmentsWPF
{
    public class GlyphTransitionItemsControl : ItemsControl
    {
        #region private classes

        private class TransitionGlyphInfo
        {
            public TransitionGlyphInfo(object item, UIElement oldGlyph)
            {
                this.Item = item;
                this.OldGlyph = oldGlyph;
                this.NewGlyph = null;
            }

            public TransitionGlyphInfo(object item, UIElement oldGlyph, UIElement newGlyph)
                : this(item, oldGlyph)
            {
                this.NewGlyph = newGlyph;
            }

            public UIElement OldGlyph;
            public UIElement NewGlyph;
            public object Item;
        }

        #endregion

        #region Dependency Properties

        #region IsItemGlyphProperty

        public static readonly DependencyProperty IsItemGlyphProperty = DependencyProperty.RegisterAttached("IsItemGlyph",
            typeof(bool), typeof(GlyphTransitionItemsControl));

        public bool IsItemGlyph
        {
            get { return (bool)this.GetValue(IsItemGlyphProperty); }
            set { this.SetValue(IsItemGlyphProperty, value); }
        }

        #endregion

        #region TransitionAnimationDurationProperty

        public static readonly DependencyProperty TransitionAnimationDurationProperty = DependencyProperty.Register("TransitionAnimationDuration",
            typeof(Duration), typeof(GlyphTransitionItemsControl),
            new FrameworkPropertyMetadata(new Duration(TimeSpan.FromMilliseconds(1000)), new PropertyChangedCallback(OnTransitionAnimationDurationChanged)));

        public Duration TransitionAnimationDuration
        {
            get { return (Duration)this.GetValue(TransitionAnimationDurationProperty); }
            set { this.SetValue(TransitionAnimationDurationProperty, value); }
        }

        private static void OnTransitionAnimationDurationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            GlyphTransitionItemsControl control = d as GlyphTransitionItemsControl;

            if (control != null)
            {
                control.transitionAnimationDuration = (Duration)e.NewValue;
            }
        }

        #endregion

        #region ContainerFadeDurationProperty

        public Duration ContainerFadeDuration
        {
            get { return (Duration)GetValue(ContainerFadeDurationProperty); }
            set { SetValue(ContainerFadeDurationProperty, value); }
        }

        // Using a DependencyProperty as the backing store for ContainerFadeDuration.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ContainerFadeDurationProperty =
            DependencyProperty.Register("ContainerFadeDuration", typeof(Duration), typeof(GlyphTransitionItemsControl),
            new FrameworkPropertyMetadata(new Duration(TimeSpan.FromMilliseconds(1000)), new PropertyChangedCallback(OnContainerFadeDurationChanged)));

        private static void OnContainerFadeDurationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            GlyphTransitionItemsControl control = d as GlyphTransitionItemsControl;

            if (control != null)
            {
                control.containerFadeDuration = (Duration)e.NewValue;
                control.CreateContainerFadeAnimation();
            }
        }

        #endregion

        #region IsTransitionCanvasProperty

        public static readonly DependencyProperty IsTransitionCanvasProperty = DependencyProperty.RegisterAttached("IsTransitionCanvas",
            typeof(bool), typeof(GlyphTransitionItemsControl), new FrameworkPropertyMetadata(new PropertyChangedCallback(OnIsTransitionCanvasChanged)));

        public static bool GetIsTransitionCanvas(Canvas canvas)
        {
            if (canvas != null)
            {
                throw new ArgumentNullException("canvas");
            }

            return (bool)canvas.GetValue(IsTransitionCanvasProperty);
        }

        public static void SetIsTransitionCanvas(Canvas canvas, bool isTransCanvas)
        {
            if (canvas != null)
            {
                throw new ArgumentNullException("canvas");
            }

            canvas.SetValue(IsTransitionCanvasProperty, isTransCanvas);
        }

        private static void OnIsTransitionCanvasChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            Canvas canvas = d as Canvas;

            if (canvas != null)
            {
                DependencyObject parent = VisualTreeHelper.GetParent(canvas);

                while (parent != null)
                {
                    GlyphTransitionItemsControl itemsControl = parent as GlyphTransitionItemsControl;

                    if (itemsControl != null)
                    {
                        itemsControl.transitionCanvas = canvas;
                        break;
                    }

                    parent = VisualTreeHelper.GetParent(parent);
                }
            }
        }

        #endregion

        #endregion

        #region events

        public event EventHandler ContainerFadeCompleted;

        private void OnContainerFadeCompleted()
        {
            if (this.ContainerFadeCompleted != null)
            {
                this.ContainerFadeCompleted(this, new EventArgs());
            }
        }

        #endregion

        #region Constructors

        public GlyphTransitionItemsControl()
            : base()
        {
            this.ItemContainerGenerator.ItemsChanged += new ItemsChangedEventHandler(ItemContainerGenerator_ItemsChanged);
            this.transitionGlyphs = new Dictionary<object, TransitionGlyphInfo>();
            this.Loaded += new RoutedEventHandler(GlyphTransitionItemsControl_Loaded);

            CreateContainerFadeAnimation();

            //grab the dispatcher
            this.dispatcher = Dispatcher.CurrentDispatcher;
        }

        #endregion

        #region private fields

        private Duration transitionAnimationDuration;
        private Duration containerFadeDuration;
        private DataTemplate oldItemTemplate;
        private DataTemplate newItemTemplate;
        private Canvas transitionCanvas;
        private DoubleAnimation containerFadeAnimation;
        private Dictionary<object, TransitionGlyphInfo> transitionGlyphs;
        private int transitionAnimationCount = 0;

        private bool itemGeneratorItemsChanged = false;
        private bool itemTemplateChanged = false;
        private bool itemAdded = false;

        private Thread workerThread;
        private bool working = true;
        private Dispatcher dispatcher;

        private Dictionary<object, DependencyObject> generatorItemContainers = new Dictionary<object, DependencyObject>();

        #endregion

        void GlyphTransitionItemsControl_Loaded(object sender, RoutedEventArgs e)
        {
            //setup the background thread
            this.workerThread = new Thread(new ThreadStart(Work));
            this.workerThread.Priority = ThreadPriority.Lowest;
            this.workerThread.IsBackground = true;

            //start working
            this.workerThread.Start();
        }

        private void Work()
        {
            while (working)
            {
                if (this.itemGeneratorItemsChanged && this.itemTemplateChanged)
                {
                    Dictionary<UIElement, AnimationTimeline[]> elementAnimations = new Dictionary<UIElement, AnimationTimeline[]>();
                    int containerCount = this.Items.Count;

                    #region Animation creation loop

                    for (int i = 0; i < containerCount; i++)
                    {
                        UIElement container;

                        try
                        {
                            container = this.ItemContainerGenerator.ContainerFromIndex(i) as UIElement;
                        }
                        catch
                        {
                            Thread.Sleep(10);
                            continue;
                        }

                        if (container != null)
                        {
                            while (!container.IsArrangeValid)
                            {
                                Thread.Sleep(10);

                                //make sure the container is still there
                                container = this.ItemContainerGenerator.ContainerFromIndex(i) as UIElement;
                            }

                            //retrieve the new glyph and it's position
                            TransitionGlyphInfo tgi = null;
                            Point newGlyphLocation = new Point();

                            //do it on the UI thread
                            this.dispatcher.Invoke(DispatcherPriority.Normal, (ThreadStart)delegate
                            {
                                UIElement newGlyph = GetItemGlyph(container);
                                object item = this.ItemContainerGenerator.ItemFromContainer(container);

                                if (newGlyph != null && this.transitionGlyphs.ContainsKey(item))
                                {
                                    tgi = this.transitionGlyphs[item];
                                    tgi.NewGlyph = newGlyph;

                                    newGlyphLocation = tgi.NewGlyph.TranslatePoint(new Point(0, 0), this.transitionCanvas);
                                }
                            });

                            if (tgi != null)
                            {
                                double newLeft = newGlyphLocation.X;
                                double newTop = newGlyphLocation.Y;

                                DoubleAnimation leftAnimation = new DoubleAnimation(newLeft, this.transitionAnimationDuration);
                                DoubleAnimation topAnimation = new DoubleAnimation(newTop, this.transitionAnimationDuration);

                                this.transitionAnimationCount++;
                                leftAnimation.Completed += new EventHandler(TransitionAnimationCompleted);

                                leftAnimation.Freeze();
                                topAnimation.Freeze();

                                elementAnimations.Add(tgi.OldGlyph, new AnimationTimeline[] { leftAnimation, topAnimation });
                            }
                        }
                    }

                    #endregion

                    this.dispatcher.Invoke(DispatcherPriority.Normal, (ThreadStart)delegate
                    {
                        foreach (UIElement element in elementAnimations.Keys)
                        {
                            AnimationTimeline[] animations = elementAnimations[element];

                            element.BeginAnimation(Canvas.LeftProperty, animations[0]);
                            element.BeginAnimation(Canvas.TopProperty, animations[1]);
                        }
                    });

                    elementAnimations.Clear();
                    this.itemGeneratorItemsChanged = false;
                    this.itemTemplateChanged = false;
                }
                else if (this.itemAdded)
                {
                    this.dispatcher.BeginInvoke(DispatcherPriority.Background, (ThreadStart)delegate
                    {
                        this.CrossFadeOldGlyphToNew();
                    });

                    itemAdded = false;
                }
                else
                {
                    Thread.Sleep(100);
                }
            }
        }

        private void ItemContainerGenerator_ItemsChanged(object sender, ItemsChangedEventArgs e)
        {
            ItemContainerGenerator generator = sender as ItemContainerGenerator;

            if (e.Action == NotifyCollectionChangedAction.Reset)
            {
                this.itemGeneratorItemsChanged = true;
            }
            else if (e.Action == NotifyCollectionChangedAction.Add)
            {
                this.itemAdded = true;
            }
        }

        private void TransitionAnimationCompleted(object sender, EventArgs e)
        {
            if (--this.transitionAnimationCount == 0)
            {
                this.CrossFadeOldGlyphToNew();
            }
        }

        private void CreateContainerFadeAnimation()
        {
            this.containerFadeAnimation = new DoubleAnimation(0, this.containerFadeDuration);
            this.containerFadeAnimation.Completed += new EventHandler(containerFadeAnimation_Completed);
        }

        private void CrossFadeOldGlyphToNew()
        {
            foreach (TransitionGlyphInfo tgi in this.transitionGlyphs.Values)
            {
                DoubleAnimation opacityAnimation = new DoubleAnimation(0, this.containerFadeDuration);

                if ((double)tgi.OldGlyph.GetValue(OpacityProperty) != 0.0)
                {
                    tgi.OldGlyph.BeginAnimation(OpacityProperty, opacityAnimation);
                }
            }

            int containerCount = this.Items.Count;
            DoubleAnimation toOpaqueAnimation = new DoubleAnimation(1, this.containerFadeDuration);

            for (int i = 0; i < containerCount; i++)
            {
                UIElement container = this.ItemContainerGenerator.ContainerFromIndex(i) as UIElement;

                if (container != null && (double)container.GetValue(OpacityProperty) == 0.0)
                {
                    container.BeginAnimation(OpacityProperty, toOpaqueAnimation);
                }
            }

            this.transitionCanvas.Children.Clear();
            this.transitionGlyphs.Clear();
        }

        private void FadeOutAllButGlyph()
        {
            int containerCount = this.Items.Count;

            List<UIElement> elementsToFade = new List<UIElement>();

            for (int i = 0; i < containerCount; i++)
            {
                UIElement container = this.ItemContainerGenerator.ContainerFromIndex(i) as UIElement;

                if (container != null && container.Visibility == Visibility.Visible)
                {
                    if (container is UIElement)
                    {
                        elementsToFade.Add(container as UIElement);
                    }

                    //grab the glyph render it to a bitmap for the transition animation and add the bitmap to the
                    //transition canvas
                    if (this.transitionCanvas != null)
                    {
                        UIElement glyph = GetItemGlyph(container);

                        if (glyph != null)
                        {
                            Rectangle glyphRect = new Rectangle();

                            glyphRect.Width = glyph.RenderSize.Width;
                            glyphRect.Height = glyph.RenderSize.Height;

                            glyphRect.Fill = GetGlyphImageDrawing(glyph); //Brushes.Purple;

                            //position the rect correctly on the canvas
                            Canvas.SetLeft(glyphRect, glyph.TranslatePoint(new Point(0, 0), this.transitionCanvas).X);
                            Canvas.SetTop(glyphRect, glyph.TranslatePoint(new Point(0, 0), this.transitionCanvas).Y);

                            //add it to the canvas
                            this.transitionCanvas.Children.Add(glyphRect);

                            //add it to the lookup
                            object item = this.ItemContainerGenerator.ItemFromContainer(container);

                            this.transitionGlyphs.Add(item, new TransitionGlyphInfo(item, glyphRect));
                        }
                    }
                }
            }

            if (elementsToFade.Count > 0)
            {
                foreach (UIElement element in elementsToFade)
                {
                    element.BeginAnimation(UIElement.OpacityProperty, containerFadeAnimation);
                }
            }
        }

        private Brush GetGlyphImageDrawing(UIElement glyph)
        {
            if (glyph == null)
            {
                throw new ArgumentNullException("glyph");
            }

            //couldn't figure another way to get the actual width and height of
            //the rendered element including any transforms on parents, etc.  So,
            //translating the top left and bottom right corners to the space
            //of the main window to get that
            //TODO: this is extremely fragile as implemented, fix so it doesn't
            //      use Application.Current.MainWindow

            double width, height;
            Point topLeft, bottomRight;

            topLeft = glyph.TranslatePoint(new Point(0, 0), Application.Current.MainWindow);
            bottomRight = glyph.TranslatePoint(new Point(glyph.RenderSize.Width, glyph.RenderSize.Height), Application.Current.MainWindow);

            width = (bottomRight.X - topLeft.X);
            height = (bottomRight.Y - topLeft.Y);

            RenderTargetBitmap bmp = new RenderTargetBitmap((int)width,
                (int)height, 96, 96, PixelFormats.Pbgra32);

            Transform oldRenderTransform = glyph.RenderTransform;
            Point oldRenderTransformOrigin = glyph.RenderTransformOrigin;

            UIElement parent = VisualTreeHelper.GetParent(glyph) as UIElement;
            Point v = glyph.TranslatePoint(new Point(0, 0), parent);

            //transform the element so it renders correctly into the target bitmap
            TransformGroup targetBitmapTransform = new TransformGroup();
            targetBitmapTransform.Children.Add(new ScaleTransform(width / glyph.RenderSize.Width, height / glyph.RenderSize.Height));
            targetBitmapTransform.Children.Add(new TranslateTransform(-v.X, -v.Y));

            glyph.RenderTransform = targetBitmapTransform;
            glyph.RenderTransformOrigin = new Point(0, 0);
            glyph.UpdateLayout();

            bmp.Render(glyph);

            glyph.RenderTransform = oldRenderTransform;
            glyph.RenderTransformOrigin = oldRenderTransformOrigin;
            glyph.UpdateLayout();

            ImageBrush brush = new ImageBrush(bmp);
            brush.Stretch = Stretch.UniformToFill;

            return brush;
        }

        internal static UIElement GetItemGlyph(DependencyObject container)
        {
            int childCount = VisualTreeHelper.GetChildrenCount(container);

            for (int i = 0; i < childCount; i++)
            {
                DependencyObject child = VisualTreeHelper.GetChild(container, i);

                if (child is ContentPresenter)
                {
                    bool test = ((ContentPresenter)child).ApplyTemplate();
                }

                if ((bool)(child.GetValue(GlyphTransitionItemsControl.IsItemGlyphProperty)))
                {
                    return child as UIElement;
                }
                else
                {
                    //check the children
                    UIElement element = GetItemGlyph(child);

                    if (element != null)
                    {
                        return element;
                    }
                }
            }

            return null;
        }

        private void containerFadeAnimation_Completed(object sender, EventArgs e)
        {
            if (this.oldItemTemplate != null && this.newItemTemplate != null)
            {
                base.OnItemTemplateChanged(this.oldItemTemplate, this.newItemTemplate);

                this.oldItemTemplate = null;
                this.newItemTemplate = null;

                this.OnContainerFadeCompleted();
                this.itemTemplateChanged = true;
            }
        }

        protected override void OnItemTemplateChanged(DataTemplate oldTemplate, DataTemplate newTemplate)
        {
            this.oldItemTemplate = oldTemplate;
            this.newItemTemplate = newTemplate;

            this.FadeOutAllButGlyph();
        }

        protected override void OnItemContainerStyleChanged(Style oldItemContainerStyle, Style newItemContainerStyle)
        {
            //add the opacity property to the style (TODO: check to make sure it's not already there)
            newItemContainerStyle.Setters.Add(new Setter(UIElement.OpacityProperty, 0.0));

            base.OnItemContainerStyleChanged(oldItemContainerStyle, newItemContainerStyle);
        }
    }
}
